
# Copyright (c) 2014, Franklin Brauning V.  All rights reserved.
# 
#    El desarrollo inicial de esta utilidad fue producido
#    por Franklin Brauning <brauningbar@gmail.com>
# 
#    Si consideras que este codigo puede serte util, eres libre
#    de usarlo a conveniencia, pero no olvides mantener la licencia
#    y dar credito al autor.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#   * Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
# 
#   * Neither the name of the author nor the names of its contributors may
#     be used to endorse or promote products derived from this software without
#     specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from .base import *
from django.db.transaction import atomic

class MergeException(AbstractException):
	''' exception raised when .merge wont succed on models generated
	by build_grouping(...)
	'''
	pass

class InvalidMerge(MergeException):
	''' raised when merges or undo/redo on models generated by
	build_grouping cant proceed because of the involved registers
	versions
	'''
	pass

class CorruptMerge(MergeException):
	''' raised on finding corrupted merging data.
	This is bad'''
	pass

# merging modes (parameter for build_grouping) for relations manytomany
#
# AT THIS POINT OF THE IMPLEMENTATION THE MERGING MODE SELECTION
# IS DONE AT MODEL CREATION.
#
MERGE_KEEP = 0 # dont bring the merged-in relations

MERGE_DUPL = 1 # if both models contain a relation to the same object,
               # the resulting register will get both relations

MERGE_UNIQ = 2  # if both models contain a relation to the same object,
                # the resulting register will get just one of them

MERGE_SOLVE = 3 # MERGE_UNIQ doesnt consider that any of the involved
                # registers could already have redundancy in its
                # relations. This flags keeps the relations really
                # unique

MERGE_CLEAN = 4 # dont merge relations (dont store any relations)

MERGE_BRING = 5 # the resulting relations will be the ones of the
                # merge argument

# The relations can be *marked*  with an always advancing pointer
# This way we can represent a *reviewed* state for the relations
# at some point in time (versioning time).
#
# AT THIS POINT OF THE IMPLEMENTATION THIS OPERATION IS MASSIVE,
# THAT IS, CHECKPOINT ALL CURRENT RELATIONS ...OR NOTHING...
# DONT GET CONFUSED TO THINK THERE ARE MULTIPLE CHECKPOINTS,
# THERE IS JUST ONE BUT CONSIDER THAT IT CAN BE UNDONE WITH .undo ...
#
# To select relations based on their checkpointed status the following
# flags can be passed to queryset-generating-methods. For example:
#
# >>> family.get_active_members(ckpoint=CKPOINT_AFTER)
# to get relations not yet checkpointed. Or:
# >>> family.get_active_members_afterckpoint()
# and
# >>> family.get_active_members(ckpoint=CKPOINT_BEFORE)
# to get already checkpointed relations. Or:
# >>> family.get_active_members_beforeckpoint()

# items added after the last checkpoint
CKPOINT_AFTER = True
# items touched at some point by a checkpoint
CKPOINT_BEFORE = False


def show_mergepts(func):
	''' composable __str__ decorator to show and debug merging state '''
	def w(self):
		s,ats = func(self)
		if self.ismergeparent():
			ats.append("*merge*")
		if self.ismergechild():
			ats.append("*backmerge*")
		if self.willundomerge():
			ats.append("backmerge")
		if self.willredomerge():
			ats.append("merge")
		return s,ats
	return w


# groupmodel: "..."
# itemmodels: ["...1", "...2", "...3", ...]
# checkpoints: [ indexes of itemmodels to be checkpointable ]
# mergingmodes: {1:MERGE_..., 2:MERGE_...}
#
def complete_grouping_names(**kw):
	import collections
	# copia flags de integracion al diccionario de plantillas (automatico)
	for n in filter(lambda n:n.startswith('MERGE_'), globals()):
		kw[n] = eval(n)

	kw['groupname'] = kw['groupmodel'].lower()
	kw['groupplural'] = kw['groupname'] + 's'
	checkpoints = kw.get('checkpoints', [])

	mergingmodes = collections.defaultdict(lambda:MERGE_DUPL)
	mergingmodes.update(kw.get('mergingmodes', {}))

	kw['items'] = []
	for i,m in enumerate(kw['itemmodels']):
		d = {'checkpoint':i in checkpoints,
		     'mergemode': mergingmodes[i]}
		if type(m) in (list, tuple):
			d['itemmodel'] = m[0]
			d['itemmodelname'] = m[0].split('.')[-1]
			d['itemname'] = m[1]
			d['itemplural'] = m[2]
		else:
			d['itemmodel'] = m
			d['itemmodelname'] = m.split('.')[-1]
			d['itemname'] = d['itemmodelname'].lower()
			d['itemplural'] = d['itemname'] + 's'

		d.update({
		  # modelo relation generado
		  'relationmodel':kw['groupmodel']+d['itemmodelname'],
		  # atributos internos
		  'added_items':'added_'+d['itemplural'],
		  'removed_items':'removed_'+d['itemplural'],
		  # generado por django en el modelo contenedor
		  'relation_set':kw['groupname']+d['itemname'] + "_set"
		})

		kw['items'].append(d)
	return kw

# MAYBE TODO:
# Support a granular checkpointing scheme in the model
# and let the caller decide if use basic checkpointing
# scheme or the granular one
#
def build_grouping(**kw):
	''' generates model definitions (string to be .exec(...)) with
	manytomany relations enriched with version control at the
	relation-level. This is an extension of fossil, considering that
	its versioning structure handles versions at register-level.

	With the here-resulting model undo/redo effect will reach at the
	relations. Positioned on any version, we can get only relations
	existent at the moment of its creation.

	Parameters:

	itemmodels: list the manytomany relation modelnames to be
	            handled with relation-level versioning.
	            Item can be in any of these forms:

	            - Just the name of the related model. Example:
	                 itemmodels=["Product", ..., ...]

	            - Specify modelname, singular name for any item of the
	            model. Example:
	                 itemmodels=[("Node", "child", "children"), ..., ...]

	groupmodel: name of the final model to actually build.
	            The afore reference model has to be defined manually.
	            In most cases this flow leverages to further extend
	            and enrich the resulting model

	groupabstract: name of the abstract model to define here. This has to
	               be subclassed to finally define the model referenced by
	               the *groupmodel* parameter

	mergingmodes (optional): spec to make a *meargeable* model.
	               In this way the resulting model with contain
	               primitives to handle the merging of registers.
	               This parameter lists wich relations and how the
	               merging code will handle them.

	               The form is a dict with keys referencing indexing
	               on the *itemmodels* parameter and the value 
	               denoting merge mode (how to handle the merge).

	               Example:
	                   itemmodels=["Product",
	                               ("Node", "child", "children"), ..., ...]
	                   mergingmodes={0:MERGE_DUPL,
	                                 2:MERGE_BRING}

	               *itemmodels* indexes not referenced get MERGE_DUPL
	               mode by default

	checkpoints:   This lists the *itemmodels* to get checkpointing code.
	               Example:

	                   itemmodels=["Product",
	                               "Pet", ..., ...]
	                   checkpoints=[1, 4]

	                   >> obj.get_active_products_afterckpoint()
	                   ...
	                   >>> obj.get_active_pets_beforeckpoint()
	                   Traceback (most recent call last):
	                   File "<stdin>", line 1, in <module>
	                   AttributeError: 'A' object has no attribute
	                   'get_active_pets_beforeckpoint'


	** The resulting string should be .exec(...) at the main level of
	a module to let Django do its own autoconfiguration stuff
	'''
	from django.template import Template, Context
	ctx = Context(complete_grouping_names(**kw))
	tmpl = Template("""
from abstract.views import fossils, actives, deleteds, tips, nmtips, \\
  mergepts, nonmergepts
{% for rel in items %}
class {{rel.relationmodel}}(models.Model):
	{{groupname}} = models.ForeignKey('{{groupmodel}}')
	{{rel.itemname}} = models.ForeignKey('{{rel.itemmodel}}', related_name='{{rel.relation_set}}')
	init_revno = models.IntegerField()
	deleted_revno = models.IntegerField(null=True)

	def __str__(self):
		return "%s - %s (%d, deleted:%s)" % (
		  str(self.{{groupname}}), str(self.{{rel.itemname}}),
		  self.init_revno, str(self.deleted_revno))

	def delete(self):
		self.deleted_revno = self.{{groupname}}.revno
		super({{rel.relationmodel}}, self).save()

	def save(self):
		self.init_revno = self.{{groupname}}.revno
		super({{rel.relationmodel}}, self).save()

{% endfor %}

class {{groupabstract}}(modifiable, deletable, fossil):
	class Meta:
		abstract = True

{% for rel in items %}
	{% if rel.checkpoint %}
	ckpoint_{{rel.itemname}}_revno = models.IntegerField(null=True, editable=False)
	{% endif %}

	# este atributo(s) no es vital para el funcionamiento interno
	# de las versiones, pero presenta la relacion a la interfaz
	# admin de django
	#
	{{rel.itemplural}} = \\
	  models.ManyToManyField('{{rel.itemmodel}}',
	  through={{rel.relationmodel}})
{% endfor %}

	{% if mergingmodes %}
	# estos enteros apuntan a las ids de los enlaces hacia los padres y el
	# hijo actual de una integracion (es opcional, tanto en la construccion
	# del metamodelo como en tiempo de ejecucion.
	#
	# Estas ids no deberian ser utilizadas por un cliente, pues en la mayoria
	# de las ocasiones ellas no apuntan hacia el nodo real de la integracion
	# sino a la punta que se ha llevado la ids del nodo generado al momento de
	# la integracion.
	# Los metodos que operan meticulosamente es el correcto mecanismo de su
	# interaccion son: merge, undo, redo
	#
	merge_child = models.IntegerField(null=True, editable=False)
	merge_child2 = models.IntegerField(null=True, editable=False)
	merge_parent1 = models.IntegerField(null=True, editable=False)
	merge_parent2 = models.IntegerField(null=True, editable=False)

	def getmerge_child(self):
		''' obtener una copia del nodo de integracion child '''
		return self.__class__.objects.get(id=self.merge_child)

	def getmerge_child2(self):
		''' obtener una copia del nodo de integracion child '''
		return self.__class__.objects.get(id=self.merge_child2)

	def getmerge_childs(self, distinct=None):
		return [self.getmerge_child()] if self.merge_child != distinct \
		       else [] + \
		       [self.getmerge_child2()] if self.merge_child2 != distinct \
		       else []

	def getmerge_parent1(self):
		''' obtener una copia del nodo de integracion parent1 '''
		return self.__class__.objects.get(id=self.merge_parent1)

	def getmerge_parent2(self):
		''' obtener una copia del nodo de integracion parent2 '''
		# XXX aca se podria agregar codigo en calidad DEBUG que pruebe
		# que los enlaces de integracion tanto de este nodo como
		# del nodo padre que esta funcion devuelve mantengan referencia
		# cruzada; de lo contrario, tambien estamos ante una integracion
		# que se ha vuelto corrupta
		#
		return self.__class__.objects.get(id=self.merge_parent2)

	def getmerge_parents(self, distinct=None):
		return [self.getmerge_parent1()] if self.merge_parent1 != distinct \
		       else [] + \
		       [self.getmerge_parent2()] if self.merge_parent2 != distinct \
		       else []

	def _reset_merges(self):
		self.merge_parent1, self.merge_parent2 = None, None
		self.merge_child, self.merge_child2 = None, None

	# XXX el nombre de estos metodos ('propagate_merges')
	# talvez no es el adecuado
	#
	def _mark_propagate_merges(self, status):
		self._propagate = status

	def _propagate_merges(self):
		return getattr(self, '_propagate', True)

	def willundomerge(self):
		return self.prevcopy and self.getprevcopy().ismergechild()

	def willredomerge(self):
		return self.nextcopy and self.getnextcopy().ismergeparent()

	def mergepointsparent(self):
		return bool(self.merge_parent1 or self.merge_parent2)

	def mergepointschild(self):
		return bool(self.merge_child or self.merge_child2)

	def ismergechild(self):
		return bool(self.merge_parent1 or self.merge_parent2)

	def ismergeparent(self):
		return bool(self.merge_child or self.merge_child2)

	def ismergepoint(self):
		return self.ismergeparent() or self.ismergechild()
	{% endif %}# endif mergingmodes

	@atomic
	def redo(self, updateobj=True, **kw):
		y = super({{groupabstract}}, self).redo(updateobj=updateobj, **kw)
		if not hasattr(y, 'merge_parent1'):
			return y
		if y.merge_child:
			child = y.getmerge_child()
			if child.merge_parent1 and child.merge_parent2:
				if child.merge_parent1==y.id:
					oth = child.getmerge_parent2()
				elif child.merge_parent2==y.id:
					oth = child.getmerge_parent1()
				else:
					# assume there is no transaction support (rollback manually)
					super({{groupabstract}}, y).undo(updateobj=updateobj)
					raise CorruptMerge("redo:punto de integracion corrupto (desface)")

				if not oth.nextcopy or oth.getnextcopy().merge_child != child.id:
					# assume there is no transaction support (rollback manually)
					super({{groupabstract}}, y).undo(updateobj=updateobj)
					raise InvalidMerge("redo:una de las cabezas no esta posicionadas en el punto " + \
					"de integracion")
				super({{groupabstract}}, oth).redo()
				super({{groupabstract}}, child).redo()
				if updateobj:
					self.__dict__ = child.__dict__
					self.modified = False
				return child
			elif child.merge_parent1 or child.merge_parent2:
				# assume there is no transaction support (rollback manually)
				super({{groupabstract}}, y).undo(updateobj=updateobj)
				raise CorruptMerge("redo:punto de integracion corrupto (un solo parent)")
		return y

	@atomic
	def undo(self, updateobj=True, **kw):
		y = super({{groupabstract}}, self).undo(updateobj=updateobj, **kw)
		if not hasattr(y, 'merge_parent1'):
			return y
		if y.merge_parent1 and y.merge_parent2:
			p1 = y.getmerge_parent1()
			p2 = y.getmerge_parent2()
			if p1.merge_child != y.id or p2.merge_child != y.id:
				# assume there is no transaction support (rollback manually)
				super({{groupabstract}}, y).redo(updateobj=updateobj)
				raise CorruptMerge("undo:punto de integracion corrupto (desface)")
			# los padres deben volver a la cabeza
			p1.current, p2.current = True, True
			super({{groupabstract}}, p1).undo()
			super({{groupabstract}}, p2).undo()
			if updateobj:
				self.__dict__ = p1.__dict__
				self.modified = False
			return p1
		elif y.merge_parent1 or y.merge_parent2:
			# assume there is no transaction support (rollback manually)
			y.redo(updateobj=updateobj)
			raise CorruptMerge("undo:punto de integracion corrupto (un solo parent)")
		return y

	@atomic
	def merge(self, oth, helpers={}):
		if self.ismergepoint() or oth.ismergepoint():
			raise InvalidMerge("merge:merge no procede en un punto de integracion")

		import itertools
		# estrategia:
		# inicializa nodo eliminado
		# agrega rels antes de ckpoint
		# save
		# marca ckpoint y agrega rels despues de ckpoint y resucita (no eliminado)
		# save.
		# Con especial cuidado en oscurecer donde corresponda (con .deleted y
		# enlaces merge) para que la coreografia undo/redo funcione
		# de la manera esperada
		#
		def dowho_helper(x,y,z):
			if isinstance(self, tracked):
				# si newnode ya tiene fue definido con datos tracked
				# posiblemente provengan de un helper. Por eso los
				# seguiremos propagando
				if z.cuser:
					z.copywho(z)
				else:
					z.copywho(self)
		def no_helper(x,y,z):
			pass

		newnode = self.dup()
		helpers.get('init1', dowho_helper)(self, oth, newnode)
		newnode.ensurenocurrent() # lo inicializamos como false en caso que esta
		                        # operacion falle no quede un nodo current sin
		                        # datos validos. De todos modos fossil.save
		                        # reactiva .current donde corresponda
		                        # en el juego .save/.undo/.redo
		newnode.delete()
		helpers.get('init2', no_helper)(self, oth, newnode)

		# la idea es agregar las relaciones pero replicaremos
		# las operaciones de agregado contra los checkpoints
		# before y after. Asi, las fases undo/redo del objeto
		# resultado recrearan discretamente estos 'eventos'
		#
		for ckpoint in [CKPOINT_BEFORE, CKPOINT_AFTER]:
			{% for rel in items %}
			self.trace("%s.merge:%s:mode %d, ckpoint %s" % (self._meta.model_name,
			  '{{rel.itemplural}}', {{rel.mergemode}}, str(ckpoint)))

						{% if rel.mergemode == MERGE_DUPL %}

			xs = itertools.chain(
			  self.get_active_{{rel.itemplural}}(ckpoint=ckpoint),
			  oth.get_active_{{rel.itemplural}}(ckpoint=ckpoint))
			dxs = itertools.chain(
			  self.get_deleted_{{rel.itemplural}}(ckpoint=ckpoint),
			  oth.get_deleted_{{rel.itemplural}}(ckpoint=ckpoint))
			for x in xs:
				newnode.add_{{rel.itemname}}(x)
			for x in dxs:
				newnode.adddeleted_{{rel.itemname}}(x)


					{% elif rel.mergemode == MERGE_KEEP %}

			xs = self.get_active_{{rel.itemplural}}(ckpoint=ckpoint)
			dxs = self.get_deleted_{{rel.itemplural}}(ckpoint=ckpoint)
			for x in xs:
				newnode.add_{{rel.itemname}}(x)
			for x in dxs:
				newnode.adddeleted_{{rel.itemname}}(x)


					{% elif rel.mergemode == MERGE_BRING %}

			xs = oth.get_active_{{rel.itemplural}}(ckpoint=ckpoint)
			dxs = oth.get_deleted_{{rel.itemplural}}(ckpoint=ckpoint)
			for x in xs:
				newnode.add_{{rel.itemname}}(x)
			for x in dxs:
				newnode.adddeleted_{{rel.itemname}}(x)


					{% elif rel.mergemode == MERGE_CLEAN %}


					{% else %}
			raise InvalidMerge("merge:modo de integracion {{rel.mergemode}} no esta definido")

					{% endif %}

			{% endfor %}

			if ckpoint==CKPOINT_BEFORE:
				helpers.get('before1', dowho_helper)(self, oth, newnode)
				newnode.ensurenocurrent()
				# para que la cantidad de versiones no sea sorpresiva
				# vamos a forzar save (a pesar que no se hayan agregado
				# relaciones)
				newnode.ensuremodified()
				newnode.undelete()
				helpers.get('before2', no_helper)(self, oth, newnode)
				# marcar checkpoints
				{% for rel in items %}
					{% if rel.checkpoint %}
				newnode.ckpoint_{{rel.itemplural}}()
					{% endif %}
				{% endfor %}

			else:
				newnode0 = newnode.getprevcopy()
				helpers.get('after1', dowho_helper)(self, oth, newnode)
				dowho_helper(0,0, self)
				dowho_helper(0,0, oth)
				# ahora oscurecemos los nodos de origen para dejar solo
				# al nodo integrado como current
				#
				self.ensurenocurrent()
				oth.ensurenocurrent()
				self.ensuremodified()
				oth.ensuremodified()
				self._mark_propagate_merges(True)
				oth._mark_propagate_merges(True)
				self.merge_child, oth.merge_child = newnode.id, newnode.id
				self.delete()
				oth.delete()
				newnode0.merge_parent1 = self.id
				newnode0.merge_parent2 = oth.id
				newnode0.rawsave()
				# para que la cantidad de versiones no sea sorpresiva
				# vamos a forzar save (a pesar que no se hayan agregado
				# relaciones)
				newnode.ensuremodified()
				newnode.save()
				# y para finalizar nos aseguramos de dejar el modelo operable
				newnode._mark_propagate_merges(True)
				helpers.get('after2', no_helper)(self, oth, newnode)

		return newnode

	def __init__(self, *args, **kwd):
{% for rel in items %}
		self.{{rel.added_items}} = []
		self.{{rel.removed_items}} = []
{% endfor %}
		self.touchcount = 0
		super({{groupabstract}}, self).__init__(*args, **kwd)

	@atomic
	def save(self, *a, **kw):
		# preprocesado de recoleccion de basura
		{% for rel in items %}
		self._scangc_{{rel.itemplural}}()
		{% endfor %}

	{% if mergingmodes %}
		# vamos a forzar la creacion de una nueva version y contra
		# esa version agregamos las relaciones
		#
		# pero antes nos aseguramos que los enlaces merge
		# no se propaguen (seria un desastre si estuviesemos
		# reactivando un nodo involucrado en un merge)
		#
		if not self._propagate_merges():
			self.microtrace("%s.save:not propagating merges" % self._meta.model_name)
			if getattr(self, 'merge_parent1', None):
				super(modifiable, self).__setattr__('merge_parent1', None)
			if getattr(self, 'merge_parent2', None):
				super(modifiable, self).__setattr__('merge_parent2', None)
			if getattr(self, 'merge_child', None):
				super(modifiable, self).__setattr__('merge_child', None)
			if getattr(self, 'merge_child2', None):
				super(modifiable, self).__setattr__('merge_child2', None)
		else:
			self.microtrace("%s.save:propagating merges" % self._meta.model_name)
	{% endif %}

		if self.touchcount:
			self.trace("%s.save/snapshot" % self._meta.model_name)
			self.ensuremodified()
			super({{groupabstract}}, self).save()
			{% for rel in items %}
			self._snapshot_{{rel.itemplural}}()
			{% endfor %}
			self.touchcount = 0
		else:
			self.trace("%s.save:nothing to snapshot" % self._meta.model_name)
			super({{groupabstract}}, self).save()

{% for rel in items %}
	{% if rel.checkpoint %}
	def ckpoint_{{rel.itemplural}}(self):
		self.ckpoint_{{rel.itemname}}_revno = self.revno
	{% endif %}

	def add_{{rel.itemname}}(self, item):
		# item, deleted, ckpointed
		self.{{rel.added_items}}.append({
		  'item':item, 'deleted':False, 'ckpointed':False})
		self.touchcount += 1

	def adddeleted_{{rel.itemname}}(self, item):
		# item, deleted, ckpointed
		self.{{rel.added_items}}.append({
		  'item':item, 'deleted':True, 'ckpointed':False})
		self.touchcount += 1

	# get a sequence of the items that will be added on .save.
	def tobeadded_{{rel.itemname}}(self, ckpoint=None):
		return (entry['item'] for entry in self.{{rel.added_items}} \
		          if not entry['deleted'])

	# get a sequence of the items that will be deleted on .save
	def tobedeleted_{{rel.itemname}}(self):
		return (entry['item'] for entry in self.{{rel.added_items}} \
		          if entry['deleted'])

	# mark current relations to be deleted on .save
	#
	def clear_{{rel.itemplural}}(self, ckpoint=None):
		xs = nonmergepts(self.get_active_{{rel.itemplural}}(ckpoint=ckpoint))
		self.{{rel.removed_items}} += [(x.id, None) for x in xs]
		self.touchcount += 1

	def clear_{{rel.itemplural}}_afterckpoint(self):
		clear_{{rel.itemplural}}(ckpoint=True)

	def clear_{{rel.itemplural}}_beforeckpoint(self):
		clear_{{rel.itemplural}}(ckpoint=False)

	def remove_{{rel.itemname}}(self, item, ckpoint=None):
		if type(item) is int:
			self.{{rel.removed_items}}.append((item, ckpoint))
			self.touchcount += 1
		elif item.id:
			self.{{rel.removed_items}}.append((item.id, ckpoint))
			self.touchcount += 1

	def get_hereadded_{{rel.itemplural}}(self):
		return map(
		  lambda r:r.{{rel.itemname}},
		  self._current_active_{{rel.itemplural}}().filter(
		    init_revno=self.revno))

	def get_heredeleted_{{rel.itemplural}}(self):
		return map(
		  lambda r:r.{{rel.itemname}},
		  self._current_{{rel.itemplural}}().filter(
		    deleted_revno=self.revno))

	def get_{{rel.itemplural}}(self, **kw):
		''' devuelve todos los itemes que tienen una relacion
		   con este grupo. Permite argumento "ckpoint"
		'''
		return map(lambda r:r.{{rel.itemname}},
		  self._current_{{rel.itemplural}}(**kw))

	def get_{{rel.itemplural}}_afterckpoint(self):
		return self.get_{{rel.itemplural}}(ckpoint=True)

	def get_{{rel.itemplural}}_beforeckpoint(self):
		return self.get_{{rel.itemplural}}(ckpoint=False)

	def get_active_{{rel.itemplural}}(self, **kw):
		''' devuelve todos los itemes con una relacion activa
		(no eliminada) con este grupo. Permite argumento "ckpoint"
		'''
		return map(lambda r:r.{{rel.itemname}},
		  self._current_active_{{rel.itemplural}}(**kw))

	def get_active_{{rel.itemplural}}_afterckpoint(self):
		return self.get_active_{{rel.itemplural}}(ckpoint=True)

	def get_active_{{rel.itemplural}}_beforeckpoint(self):
		return self.get_active_{{rel.itemplural}}(ckpoint=False)

	# metodo conveniente para acceder desde las plantillas a la punta
	# de las relaciones...
	def get_fossil_{{rel.itemplural}}(self, **kw):
		''' devuelve todos los itemes con una relacion activa
		(no eliminada) y de esos items presentar solo aquellos
		que sean punta. Permite argumento "ckpoint"
		'''
		return fossils(nonmergepts(self.get_active_{{rel.itemplural}}(**kw)))

	def get_fossil_{{rel.itemplural}}_afterckpoint(self):
		return self.get_fossil_{{rel.itemplural}}(ckpoint=True)

	def get_fossil_{{rel.itemplural}}_beforeckpoint(self):
		return self.get_fossil_{{rel.itemplural}}(ckpoint=False)

	def get_tip_{{rel.itemplural}}(self, **kw):
		''' ...
		    Permite argumento "ckpoint"
		'''
		return nmtips(self.get_active_{{rel.itemplural}}(**kw))

	def get_tip_{{rel.itemplural}}_afterckpoint(self):
		return self.get_tip_{{rel.itemplural}}(ckpoint=True)

	def get_tip_{{rel.itemplural}}_beforeckpoint(self):
		return self.get_tip_{{rel.itemplural}}(ckpoint=False)

	def get_deleted_{{rel.itemplural}}(self, **kw):
		''' devuelve todos los items con una relacion anulada
		(eliminada) con este grupo. Tambien permite argumento
		"ckpoint"
		'''
		return map(lambda r:r.{{rel.itemname}},
		  self._current_deleted_{{rel.itemplural}}(**kw))

	def get_deleted_{{rel.itemplural}}_afterckpoint(self):
		return self.get_deleted_{{rel.itemplural}}(ckpoint=True)

	def get_deleted_{{rel.itemplural}}_beforeckpoint(self):
		return self.get_deleted_{{rel.itemplural}}(ckpoint=False)

	# si ckpoint es bool:
	#   Se considera el hito checkpoint
	#   con True: los itemes que entraron despues del ultimo checkpoint
	#   con False: los items que se 'tocaron' con el ultimo checkpoint
	#
	def _current_{{rel.itemplural}}(self, ckpoint=None):
		{% if groupmodel == rel.itemmodelname %}
		# XXX: esta relacion usualmente es self.{{rel.relation_set}} pero
		# dado que el atributo gestionado por este metodo es recursivo,
		# todas las relaciones se agrupan *solo* bajo este atributo.
		# Este agregado se adapto de urgencia para la definicion del modelo Categoria...
		# lo mas probable es que con cualquier otro modelo recursivo esta manera
		# de acceder a tales relaciones es incorrecta y producira bugs
		#
		r = self.{{groupname}}{{groupname}}_set
		{% else %}
		r = self.{{rel.relation_set}}
		{% endif %}
		xs = r.filter(init_revno__lte=self.revno)

		if type(ckpoint) is bool:
			{% if rel.checkpoint %}
			if self.ckpoint_{{rel.itemname}}_revno:
				if ckpoint:
					return xs.filter(
					  init_revno__gt=self.ckpoint_{{rel.itemname}}_revno)
				else:
					return xs.filter(
					  init_revno__lte=self.ckpoint_{{rel.itemname}}_revno)
			{% endif %}
			return xs if ckpoint else xs.none()
		return xs

	def _current_active_{{rel.itemplural}}(self, **kw):
		return self._current_{{rel.itemplural}}(**kw).filter(
		  Q(deleted_revno__isnull=True) |
		  Q(deleted_revno__gt=self.revno))

	def _current_deleted_{{rel.itemplural}}(self, **kw):
		return self._current_{{rel.itemplural}}(**kw).filter(
		  deleted_revno__isnull=False,
		  deleted_revno__lte=self.revno)

	def _scangc_{{rel.itemplural}}(self):
		rels = self.{{rel.relation_set}}.filter({{groupname}}=self)
		# los items eliminados despues de esta version se vuelven a
		# activar
		future_deleteds = rels.filter(
		  deleted_revno__gt=self.revno)

		# los items agregados despues de esta version se eliminan
		future_adds = rels.filter(
		  init_revno__gt=self.revno)

		#print("gc over %d:%s" % (self.revno, str(self)))
		#print("future deleted {{rel.itemplural}} " + str(future_deleteds))
		#print("future {{rel.itemplural}} " + str(future_adds))
		# nota: atacar primero las eliminaciones y luego los
		# agregados. Porque un item que este eliminado en el futuro
		# puede tambien estar agregado en el futuro
		future_deleteds.update(deleted_revno=None)
		future_adds.delete()


	def _snapshot_{{rel.itemplural}}(self):
		added_items = self.{{rel.added_items}}
		removed_items = self.{{rel.removed_items}}
		if added_items:
			objs = list(map(
			  lambda data:{{rel.relationmodel}}(
			    {{groupname}}=self, {{rel.itemname}}=data['item'],
			    init_revno=self.revno,
			    deleted_revno=self.revno if data['deleted'] else None),
			  added_items))

			{{rel.relationmodel}}.objects.bulk_create(objs)
			added_items.clear()

		if removed_items:
			rels = self._current_active_{{rel.itemplural}}()
			for itemid,ckpoint in removed_items:
				if type(ckpoint) is bool and \
				        not self.ckpoint_{{rel.itemname}}_revno and \
				        not ckpoint:
				    # eliminar aquellos antes de checkpoint, pero
				    # no hay checkpoint
				    continue
				xs = rels.filter({{rel.itemname}}__id=itemid)

				if ckpoint == True and self.ckpoint_{{rel.itemname}}_revno:
					xs = xs.filter(
					  init_revno__gt=self.ckpoint_{{rel.itemname}}_revno)
				elif ckpoint == False:
					xs = xs.filter(
					  init_revno__lte=self.ckpoint_{{rel.itemname}}_revno)

				x = xs.last()
				if x:
					x.delete()

			removed_items.clear()
{% endfor %}
""")

	return tmpl.render(ctx)


